# frozen_string_literal: true

default_platform(:mac)
fastlane_require 'dotenv'
fastlane_require 'shellwords' # used for shell-escaping paths

UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler?

# Constants
USER_ENV_FILE_PATH = File.join(Dir.home, '.simplenotemacos-env.default')
PROJECT_FOLDER = Pathname.new(File.join(Dir.pwd, '..')).expand_path.to_s
PROJECT_ROOT_FOLDER = PROJECT_FOLDER
PROJECT = 'Simplenote.xcodeproj'
INTERNAL_SCHEME = 'Simplenote'
APP_STORE_SCHEME = 'Simplenote'
BUILD_FOLDER = File.join(PROJECT_FOLDER, 'build')
APP_STORE_BUNDLE_IDENTIFIER = 'com.automattic.SimplenoteMac'
APP_STORE_BUNDLE_IDENTIFIER_INTENTS = "#{APP_STORE_BUNDLE_IDENTIFIER}.IntentsExtension".freeze
VERSION_FILE_PATH = File.join(PROJECT_FOLDER, 'config', 'Version.Public.xcconfig')
DEFAULT_BRANCH = 'trunk'
GITHUB_REPO = 'Automattic/simplenote-macos'
APPLE_TEAM_ID = 'PZYM8XX95Q'

APP_RESOURCES_DIR = File.join(PROJECT_ROOT_FOLDER, 'Simplenote', 'Resources')
RELEASE_NOTES_SOURCE_PATH = File.join(APP_RESOURCES_DIR, 'release_notes.txt')
STORE_METADATA_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'metadata')

GLOTPRESS_BASE_URL = 'https://translate.wordpress.com/projects'
# Notice the trailing / is required.
# Without it, GlotPress will redirect to the version with /
GLOTPRESS_APP_STRINGS_PROJECT_URL = "#{GLOTPRESS_BASE_URL}/simplenote/macos/".freeze
GLOTPRESS_STORE_METADATA_PROJECT_URL = "#{GLOTPRESS_APP_STRINGS_PROJECT_URL}release-notes/".freeze

# Mapping of all locales which can be used for AppStore metadata (Glotpress code => AppStore Connect code)
#
# TODO: Replace with `LocaleHelper` once provided by release toolkit (https://github.com/wordpress-mobile/release-toolkit/pull/296)
GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES = {
  'ar' => 'ar-SA',
  'de' => 'de-DE',
  'es' => 'es-ES',
  'fr' => 'fr-FR',
  'he' => 'he',
  'id' => 'id',
  'it' => 'it',
  'ja' => 'ja',
  'ko' => 'ko',
  'nl' => 'nl-NL',
  'pt-br' => 'pt-BR',
  'ru' => 'ru',
  'sv' => 'sv',
  'tr' => 'tr',
  'zh-cn' => 'zh-Hans',
  'zh-tw' => 'zh-Hant'
}.freeze

########################################################################
# Environment
########################################################################
Dotenv.load(USER_ENV_FILE_PATH)
ENV['PROJECT_NAME'] = 'Simplenote'
ENV['PROJECT_ROOT_FOLDER'] = './'
ENV['APP_STORE_STRINGS_FILE_NAME'] = 'AppStoreStrings.pot'
ENV['PUBLIC_CONFIG_FILE'] = './config/Version.public.xcconfig'
ENV['DOWNLOAD_METADATA'] = './fastlane/download_metadata.swift'
ENV['FL_RELEASE_TOOLKIT_DEFAULT_BRANCH'] = 'trunk'

# Use this instead of getting values from ENV directly
# It will throw an error if the requested value is missing
def get_required_env(key)
  UI.user_error!("Environment variable '#{key}' is not set. Have you setup #{USER_ENV_FILE_PATH} correctly?") unless ENV.key?(key)
  ENV.fetch(key)
end

# On CI, it's useful to skip code signing unless necessary to avoid spending
# time syncing the certificates and profiles with match. And while not
# neccessary, it doesn't hurt on a dev machine either.
SKIP_CODE_SIGNING_XCARGS = {
  COMPILER_INDEX_STORE_ENABLE: 'NO',
  CODE_SIGN_IDENTITY: '',
  CODE_SIGNING_REQUIRED: 'NO'
}.freeze

before_all do
  # Ensure we use the latest version of the Release Toolkit
  check_for_toolkit_updates unless is_ci || ENV['FASTLANE_SKIP_TOOLKIT_UPDATE_CHECK']

  # Check that the env files exist
  unless is_ci || File.file?(USER_ENV_FILE_PATH)
    error_message = <<~ERROR
      #{USER_ENV_FILE_PATH} not found. Please copy env/user.env-example to #{USER_ENV_FILE_PATH} and fill in the values") using the following command:
      \tcp #{PROJECT_FOLDER}/fastlane/env/user.env-example #{USER_ENV_FILE_PATH}
    ERROR
    UI.user_error!(error_message)
  end

  setup_ci # Fixes weird Keychain bugs on CI
end

# Helper function to DRY the code to build the app
def build_simplenote(codesign:, archive_path: nil)
  if codesign
    skip_archive = false
    xcargs = []
  else
    # Skip archiving when not codesigning
    skip_archive = true
    xcargs = SKIP_CODE_SIGNING_XCARGS
  end

  build_mac_app(
    project: PROJECT,
    scheme: APP_STORE_SCHEME,
    clean: true,
    output_directory: BUILD_FOLDER,
    archive_path: archive_path,
    skip_archive: skip_archive,
    export_team_id: APPLE_TEAM_ID,
    xcargs: xcargs,
    export_options: {
      method: 'app-store',
      manageAppVersionAndBuildNumber: false
    }
  )
end

desc 'Runs the unit tests for the project'
lane :test do
  run_tests(
    project: PROJECT,
    scheme: INTERNAL_SCHEME,
    configuration: 'Debug',
    clean: true,
    xcargs: SKIP_CODE_SIGNING_XCARGS
  )
end

desc 'Builds the app using the App Store target but without code signing, just to check the target builds'
lane :test_app_store_build do
  configure_apply
  build_simplenote(codesign: false)
end

########################################################################
# Release Lanes
########################################################################
#####################################################################################
# code_freeze
# -----------------------------------------------------------------------------------
# This lane executes the steps planned on code freeze
# -----------------------------------------------------------------------------------
# Usage:
# bundle exec fastlane code_freeze [skip_confirm:<skip confirm>]
#
# Example:
# bundle exec fastlane code_freeze
# bundle exec fastlane code_freeze skip_confirm:true
#####################################################################################
desc 'Creates a new release branch from the current trunk'
lane :code_freeze do |options|
  ios_codefreeze_prechecks(options)

  ios_bump_version_release
  new_version = ios_get_app_version(public_version_xcconfig_file: VERSION_FILE_PATH)
  ios_update_release_notes(new_version: new_version)

  UI.important('Pushing changes to remote and configuring the release on GitHub')
  UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") unless options[:skip_confirm] || UI.confirm('Do you want to continue?')

  push_to_git_remote(tags: false)

  copy_branch_protection(
    repository: GITHUB_REPO,
    from_branch: DEFAULT_BRANCH,
    to_branch: "release/#{new_version}"
  )
  setfrozentag(
    repository: GITHUB_REPO,
    milestone: new_version
  )

  generate_strings_file_for_glotpress
end

#####################################################################################
# update_appstore_strings
# -----------------------------------------------------------------------------------
# This lane updates the AppStoreStrings.pot files with the latest content from
# the release_notes.txt file and the other text sources
# -----------------------------------------------------------------------------------
# Usage:
# bundle exec fastlane update_appstore_strings version:<release note version>
#
# Example:
# bundle exec fastlane update_appstore_strings version:1.1
#####################################################################################
desc 'Updates the AppStoreStrings.pot file with the latest data'
lane :update_appstore_strings do |options|
  source_metadata_folder = File.join(PROJECT_FOLDER, 'fastlane', 'appstoreres', 'metadata', 'source')

  files = {
    whats_new: File.join(PROJECT_FOLDER, 'Simplenote', 'Resources', 'release_notes.txt'),
    app_store_subtitle: File.join(source_metadata_folder, 'subtitle.txt'),
    app_store_desc: File.join(source_metadata_folder, 'description.txt'),
    app_store_keywords: File.join(source_metadata_folder, 'keywords.txt')
  }

  ios_update_metadata_source(
    po_file_path: File.join(PROJECT_FOLDER, 'Simplenote', 'Resources', 'AppStoreStrings.pot'),
    source_files: files,
    release_version: options[:version]
  )
end

#####################################################################################
# new_beta_release
# -----------------------------------------------------------------------------------
# This lane updates the release branch for a new beta release. It will update the
# current release branch by default. If you want to update a different branch
# (i.e. hotfix branch) pass the related version with the 'base_version' param
# (example: base_version:10.6.1 will work on the 10.6.1 branch)
# -----------------------------------------------------------------------------------
# Usage:
# bundle exec fastlane new_beta_release [skip_confirm:<skip confirm>] [base_version:<version>]
#
# Example:
# bundle exec fastlane new_beta_release
# bundle exec fastlane new_beta_release skip_confirm:true
# bundle exec fastlane new_beta_release base_version:10.6.1
#####################################################################################
desc 'Updates a release branch for a new beta release'
lane :new_beta_release do |options|
  ios_betabuild_prechecks(options)
  generate_strings_file_for_glotpress
  download_strings_files_from_glotpress
  ios_bump_version_beta
  ios_tag_build
end

#####################################################################################
# new_hotfix_release
# -----------------------------------------------------------------------------------
# This lane creates the release branch for a new hotfix release.
# -----------------------------------------------------------------------------------
# Usage:
# bundle exec fastlane new_hotfix_release [skip_confirm:<skip confirm>] [version:<version>]
#
# Example:
# bundle exec fastlane new_hotfix_release version:10.6.1
# bundle exec fastlane new_hotfix_release skip_confirm:true version:10.6.1
#####################################################################################
desc 'Creates a new hotfix branch for the given version:x.y.z. The branch will be cut from the tag x.y of the previous release'
lane :new_hotfix_release do |options|
  prev_ver = ios_hotfix_prechecks(options)
  ios_bump_version_hotfix(
    previous_version: prev_ver,
    version: options[:version]
  )
end

#####################################################################################
# finalize_hotfix_release
# -----------------------------------------------------------------------------------
# This lane finalizes the hotfix branch.
# -----------------------------------------------------------------------------------
# Usage:
# bundle exec fastlane finalize_hotfix_release [skip_confirm:<skip confirm>]
#
# Example:
# bundle exec fastlane finalize_hotfix_release skip_confirm:true
#####################################################################################
desc 'Performs the final checks and triggers a release build for the hotfix in the current branch'
lane :finalize_hotfix_release do |options|
  ios_finalize_prechecks(options)
  version = ios_get_app_version(public_version_xcconfig_file: VERSION_FILE_PATH)
  trigger_release_build(branch_to_build: "release/#{version}")
end

#####################################################################################
# finalize_release
# -----------------------------------------------------------------------------------
# This lane finalize a release: updates store metadata, pushes the final tag and
# cleans all the temp ones
# -----------------------------------------------------------------------------------
# Usage:
# bundle exec fastlane finalize_release [skip_confirm:<skip confirm>] [version:<version>]
#
# Example:
# bundle exec fastlane finalize_release
# bundle exec fastlane finalize_release skip_confirm:true
#####################################################################################
desc 'Does the necessary build and metadata updates then triggers an App Store deployment'
lane :finalize_release do |options|
  UI.user_error!('To finalize a hotfix, please use the finalize_hotfix_release lane instead') if ios_current_branch_is_hotfix

  ios_finalize_prechecks(options)

  download_strings_files_from_glotpress
  ios_bump_version_beta

  trigger_release_build(branch_to_build: "release/#{ios_get_app_version(public_version_xcconfig_file: VERSION_FILE_PATH)}")
end

# This lane builds the app using a Developer ID certificate for distributing internally.
#
# Note: Historically, this lane was used to not only build but also upload the app to App Center.
# App Center has since been discontinued, and Firebase App Distribution and its corresponding fastlane plugin
# (which we use as a replacement for iOS apps) does not seem to support macOS `.app` binaries.
#
# Thus at the moment this lane only builds the `.app` and sign it with a Developer ID certificate but lets the caller
# decide how to distribute it. If there's really a need for distributing those kind of builds more often in the future,
# we could consider using [Apps CDN](https://fieldguide.automattic.com/apps-cdn-technical-documentation/)
# and `upload_build_to_apps_cdn` to distribute them.
#
desc 'Builds and updates for beta distribution'
lane :build_internal_beta do |skip_dsym_upload: true|
  ensure_git_status_clean

  configure_apply
  configure_code_signing_developer_id

  build_app(
    project: PROJECT,
    scheme: INTERNAL_SCHEME,
    clean: true,
    output_directory: BUILD_FOLDER,
    export_method: 'developer-id',
    export_options: {
      provisioningProfiles: {
        "#{APP_STORE_BUNDLE_IDENTIFIER}": "match Direct #{APP_STORE_BUNDLE_IDENTIFIER} macos",
        "#{APP_STORE_BUNDLE_IDENTIFIER_INTENTS}": "match Direct #{APP_STORE_BUNDLE_IDENTIFIER_INTENTS} macos"
      },
      signingSytle: 'manual'
    }
  )

  # Show release notes and the path to the binary
  sh('ruby', '../Scripts/extract_release_notes.rb', '-k')
  UI.success("DeveloperID-signed binary is at: #{File.join(BUILD_FOLDER, 'Simplenote.app')}")

  dsym_path = File.join(BUILD_FOLDER, 'Simplenote.app.dSYM.zip')

  next if skip_dsym_upload || !UI.confirm('Do you want to upload the dSYM to Sentry?')

  sentry_upload_dsym(
    auth_token: get_required_env('SENTRY_AUTH_TOKEN'),
    org_slug: 'a8c',
    project_slug: 'simplenote-macos',
    dsym_path: dsym_path
  )
end

# Upload the localized metadata (from `fastlane/metadata/`) to App Store Connect
#
# @option [Boolean] with_screenshots (default: false) If true, will also upload the latest screenshot files to ASC
#
desc 'Upload the localized metadata to App Store Connect, optionally including screenshots.'
lane :update_metadata_on_app_store_connect do |with_screenshots: false|
  # Skip screenshots by default. The naming is "with" to make it clear that
  # callers need to opt-in to adding screenshots. The naming of the deliver
  # (upload_to_app_store) parameter, on the other hand, uses the skip verb.
  skip_screenshots = !with_screenshots

  upload_to_app_store(
    app_identifier: APP_STORE_BUNDLE_IDENTIFIER,
    app_version: ios_get_app_version(public_version_xcconfig_file: VERSION_FILE_PATH),
    skip_binary_upload: true,
    platform: 'osx',
    metadata_path: File.join(PROJECT_FOLDER, 'fastlane', 'metadata'),
    screenshots_path: './screenshots/',
    skip_screenshots: skip_screenshots,
    overwrite_screenshots: true, # won't have effect if `skip_screenshots` is true
    phased_release: true,
    precheck_include_in_app_purchases: false,
    api_key: app_store_connect_api_key,
    copyright: "© #{Time.now.year} Automattic, Inc."
  )
end

#############################################################################
# build_and_upload_app_store
# ---------------------------------------------------------------------------
# This lane builds the app and uploads it to the Mac App Store, optionally
# creating a GitHub release.
# ---------------------------------------------------------------------------
# Usage:
# bundle exec fastlane build_and_upload_app_store [create_github_release:<create github release>]
#
# Example:
# bundle exec fastlane build_and_upload_app_store
# bundle exec fastlane build_and_upload_app_store create_github_release:true
#############################################################################
lane :build_and_upload_app_store do |options|
  configure_apply

  setup_ci

  configure_code_signing_app_store

  archive_path = File.join(BUILD_FOLDER, 'Simplenote-Mac.xcarchive')
  build_simplenote(
    codesign: true,
    archive_path: archive_path
  )

  sentry_upload_dsym(
    auth_token: get_required_env('SENTRY_AUTH_TOKEN'),
    org_slug: 'a8c',
    project_slug: 'simplenotemacos',
    # At the time of writing, there's no way to explicitly configure the
    # dSYM path, but build_mac_app sets it in the environment if successful.
    # See `bundle exec fastlane action build_mac_app`.
    dsym_path: ENV.fetch('DSYM_OUTPUT_PATH', nil)
  )

  # Do not create the GitHub release unless explicitly requested
  if options[:create_github_release]
    archive_zip_path = "#{archive_path}.zip"
    zip(path: archive_path, output_path: archive_zip_path)

    create_release(
      repository: GITHUB_REPO,
      version: ios_get_app_version(public_version_xcconfig_file: VERSION_FILE_PATH),
      release_notes_file_path: 'Simplenote/Resources/release_notes.txt',
      release_assets: [archive_zip_path],
      prerelease: options[:prerelease]
    )
  end

  upload_to_testflight(api_key: app_store_connect_api_key)
end

lane :trigger_release_build do
  UI.important('Will not trigger release build because Simplenote macOS was never configured to build in CI.')
  # For future reference:
  #
  # buildkite_trigger_build(
  #   buildkite_organization: 'automattic',
  #   buildkite_pipeline: 'simplenote-macos',
  #   branch: branch_to_build || git_branch,
  #   pipeline_file: 'release-builds.yml'
  # )
end

desc 'Updates the main `Localizable.strings` file — that will be imported by GlotPress'
lane :generate_strings_file_for_glotpress do
  en_lproj_path = File.join(PROJECT_FOLDER, 'Simplenote', 'en.lproj')
  ios_generate_strings_file_from_code(
    paths: ['Simplenote', 'Pods/Simperium-OSX/*/SPAuthentication*'],
    output_dir: en_lproj_path,
    quiet: true
  )

  # Convert the UTF-16 encoded strings file to UTF-8.
  strings_file_path = File.join(en_lproj_path, 'Localizable.strings')
  utf8_strings = File.read(strings_file_path, encoding: 'UTF-16', mode: 'rb')
  File.write(strings_file_path, utf8_strings, mode: 'w', encoding: 'UTF-8')

  git_commit(
    path: en_lproj_path,
    message: 'Freeze strings for localization',
    allow_nothing_to_commit: true
  )
end

desc 'Download strings from from Glotpress'
lane :download_strings_files_from_glotpress do
  locale_codes = {
    'ar' => 'ar',             # Arabic
    'cy' => 'cy',             # Welsh
    'de' => 'de',             # German
    'el' => 'el',             # Greek
    'es' => 'es',             # Spanish
    'fa' => 'fa',             # Persian
    'fr' => 'fr',             # French
    'he' => 'he',             # Hebrew
    'id' => 'id',             # Indonesian
    'it' => 'it',             # Italian
    'ja' => 'ja',             # Japanese
    'ko' => 'ko',             # Korean
    'nl' => 'nl',             # Dutch
    'pt-br' => 'pt-BR',       # Portuguese (Brazil)
    'ru' => 'ru',             # Russian
    'sv' => 'sv',             # Swedish
    'tr' => 'tr',             # Turkish
    'zh-cn' => 'zh-Hans-CN',  # Chinese (China)
    'zh-tw' => 'zh-Hant-TW'   # Chinese (Taiwan)
  }.freeze
  ios_download_strings_files_from_glotpress(
    project_url: 'https://translate.wordpress.com/projects/simplenote/macos/',
    locales: locale_codes,
    download_dir: File.join(PROJECT_FOLDER, 'Simplenote')
  )
end

lane :configure_code_signing do |readonly: true|
  configure_code_signing_developer_id(readonly: readonly)
  configure_code_signing_app_store(readonly: readonly)
end

lane :configure_code_signing_developer_id do |readonly: true|
  configure_code_signing(type: 'developer_id', readonly: readonly)
end

lane :configure_code_signing_app_store do |readonly: true|
  configure_code_signing(type: 'appstore', readonly: readonly)
end

def configure_code_signing(type:, readonly: true)
  access_key = get_required_env('MATCH_S3_ACCESS_KEY')
  secret_access_key = get_required_env('MATCH_S3_SECRET_ACCESS_KEY')

  sync_code_signing(
    storage_mode: 's3',
    s3_bucket: 'a8c-fastlane-match',
    s3_region: 'us-east-2',
    s3_access_key: access_key,
    s3_secret_access_key: secret_access_key,
    team_id: APPLE_TEAM_ID,
    type: type,
    platform: 'macos',
    readonly: readonly,
    app_identifier: [
      APP_STORE_BUNDLE_IDENTIFIER,
      APP_STORE_BUNDLE_IDENTIFIER_INTENTS
    ],
    api_key: app_store_connect_api_key
  )
end

lane :download_localized_metadata_from_glotpress do
  # FIXME: We should make the `fastlane/metadata/default/release_notes.txt` path be the source of truth for the original copies in the future.
  # (will require changes in the `update_appstore_strings` lane, the Release Scenario, the MC tool to generate the announcement post…)
  #
  # In the meantime, just copy the file to the right place for `deliver` to find, for the `default` pseudo-locale which is used as fallback
  FileUtils.cp(RELEASE_NOTES_SOURCE_PATH, File.join(STORE_METADATA_FOLDER, 'default', 'release_notes.txt'))

  # FIXME: Replace this with a call to the future replacement of `gp_downloadmetadata` once it's implemented in the release-toolkit (see paaHJt-31O-p2).
  target_files = {
    "v#{release_version_current}-whats-new": { desc: 'release_notes.txt', max_size: 4000 },
    app_store_name: { desc: 'name.txt', max_size: 30 },
    app_store_subtitle: { desc: 'subtitle.txt', max_size: 30 },
    app_store_desc: { desc: 'description.txt', max_size: 4000 },
    app_store_keywords: { desc: 'keywords.txt', max_size: 100 }
  }
  gp_downloadmetadata(
    project_url: GLOTPRESS_STORE_METADATA_PROJECT_URL,
    target_files: target_files,
    locales: GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES,
    download_path: STORE_METADATA_FOLDER
  )

  files_to_commit = [File.join(STORE_METADATA_FOLDER, '**', '*.txt')]

  ensure_default_metadata_are_not_overridden(metadata_files_hash: target_files)

  files_to_commit.append(*generate_gitkeep_for_empty_locale_folders)

  git_add(path: files_to_commit, shell_escape: false)
  git_commit(
    path: files_to_commit,
    message: 'Update App Store metadata translations',
    allow_nothing_to_commit: true
  )
end

# FIXME: Defined this way to ease transition to Versioning module while copying code ported from repos already using it
def release_version_current
  ios_get_app_version(public_version_xcconfig_file: VERSION_FILE_PATH)
end

# Ensure that none of the `.txt` files in `en-US` would accidentally override our originals in `default`
def ensure_default_metadata_are_not_overridden(metadata_files_hash:)
  metadata_files_hash.values.map { |t| t[:desc] }.each do |file|
    en_file_path = File.join(STORE_METADATA_FOLDER, 'en-US', file)

    override_not_allowed_message = <<~MSG
      File `#{en_file_path}` would override the same one in `#{STORE_METADATA_FOLDER}/default`.
      `default/` is the source of truth and we cannot allow it to change unintentionally.
      Delete `#{en_file_path}`, ensure the version in `default/` has the expected original copy, and try again.
    MSG
    UI.user_error!(override_not_allowed_message) if File.exist?(en_file_path)
  end
end

# Ensure even empty locale folders have an empty `.gitkeep` file.
# This way, if we don't have any translation ready for those locales, we'll still have the folders in Git for clarity.
def generate_gitkeep_for_empty_locale_folders
  gitkeeps = []

  GLOTPRESS_TO_ASC_METADATA_LOCALE_CODES.each_value do |locale|
    gitkeep = File.join(STORE_METADATA_FOLDER, locale, '.gitkeep')
    next if File.exist?(gitkeep)

    FileUtils.mkdir_p(File.dirname(gitkeep))
    FileUtils.touch(gitkeep)
    gitkeeps.append(gitkeep)
  end

  gitkeeps
end
